package io.ebeaninternal.server.transaction;

import io.ebean.config.ExternalTransactionManager;
import io.ebean.util.JdbcClose;
import io.ebeaninternal.api.SpiTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.TransactionSynchronizationRegistry;
import javax.transaction.UserTransaction;

/**
 * Hook into external JTA transaction manager.
 */
public class JtaTransactionManager implements ExternalTransactionManager {

  private static final Logger logger = LoggerFactory.getLogger(JtaTransactionManager.class);

  private static final String EBEAN_TXN_RESOURCE = "EBEAN_TXN_RESOURCE";

  /**
   * The Ebean transaction manager.
   */
  private TransactionManager transactionManager;

  /**
   * The EbeanServer name.
   */
  private String serverName;

  /**
   * Instantiates a new spring aware transaction scope manager.
   */
  public JtaTransactionManager() {
  }

  /**
   * Initialise this with the Ebean internal transaction manager.
   */
  @Override
  public void setTransactionManager(Object txnMgr) {

    // RB: At this stage not exposing TransactionManager to
    // the public API and hence the Object type and casting here

    this.transactionManager = (TransactionManager) txnMgr;
    this.serverName = transactionManager.getServerName();
  }

  /**
   * Return the current dataSource taking into account multi-tenancy.
   */
  private DataSource dataSource() {
    return transactionManager.getDataSource();
  }

  private TransactionSynchronizationRegistry getSyncRegistry() {
    try {
      InitialContext ctx = new InitialContext();
      return (TransactionSynchronizationRegistry) ctx.lookup("java:comp/TransactionSynchronizationRegistry");
    } catch (NamingException e) {
      throw new PersistenceException(e);
    }
  }

  private UserTransaction getUserTransaction() {
    try {
      InitialContext ctx = new InitialContext();
      return (UserTransaction) ctx.lookup("java:comp/UserTransaction");
    } catch (NamingException e) {
      // assuming CMT
      return new DummyUserTransaction();
    }
  }

  /**
   * Looks for a current Spring managed transaction and wraps/returns that as a Ebean transaction.
   * <p>
   * Returns null if there is no current spring transaction (lazy loading outside a spring txn etc).
   * </p>
   */
  @Override
  public Object getCurrentTransaction() {

    TransactionSynchronizationRegistry syncRegistry = getSyncRegistry();

    SpiTransaction t = (SpiTransaction) syncRegistry.getResource(EBEAN_TXN_RESOURCE);
    if (t != null) {
      // we have already seen this transaction
      return t;
    }

    // check current Ebean transaction
    SpiTransaction currentEbeanTransaction = DefaultTransactionThreadLocal.get(serverName);
    if (currentEbeanTransaction != null) {
      // NOT expecting this so log WARNING
      String msg = "JTA Transaction - no current txn BUT using current Ebean one " + currentEbeanTransaction.getId();
      logger.warn(msg);
      return currentEbeanTransaction;
    }

    UserTransaction ut = getUserTransaction();
    if (ut == null) {
      // no current JTA transaction
      if (logger.isDebugEnabled()) {
        logger.debug("JTA Transaction - no current txn");
      }
      return null;
    }

    // This is a transaction that Ebean has not seen before.

    // "wrap" it in a Ebean specific JtaTransaction
    String txnId = String.valueOf(System.currentTimeMillis());
    JtaTransaction newTrans = new JtaTransaction(txnId, true, ut, dataSource(), transactionManager);

    // create and register transaction listener
    JtaTxnListener txnListener = createJtaTxnListener(newTrans);

    syncRegistry.putResource(EBEAN_TXN_RESOURCE, newTrans);
    syncRegistry.registerInterposedSynchronization(txnListener);

    // also put in Ebean ThreadLocal
    DefaultTransactionThreadLocal.set(serverName, newTrans);
    return newTrans;
  }


  /**
   * Create a listener to register with JTA to enable Ebean to be
   * notified when transactions commit and rollback.
   * <p>
   * This is used by Ebean to notify it's appropriate listeners and maintain it's server
   * cache etc.
   * </p>
   */
  private JtaTxnListener createJtaTxnListener(SpiTransaction t) {
    return new JtaTxnListener(transactionManager, t);
  }

  private static class DummyUserTransaction implements UserTransaction {

    @Override
    public void begin() throws NotSupportedException, SystemException {
    }

    @Override
    public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException,
      SecurityException, IllegalStateException, SystemException {
    }

    @Override
    public int getStatus() throws SystemException {
      return 0;
    }

    @Override
    public void rollback() throws IllegalStateException, SecurityException, SystemException {
    }

    @Override
    public void setRollbackOnly() throws IllegalStateException, SystemException {
    }

    @Override
    public void setTransactionTimeout(int seconds) throws SystemException {
    }
  }

  /**
   * A JTA Transaction Synchronization that we register to get notified when a
   * managed transaction has been committed or rolled back.
   * <p>
   * When Ebean is notified (of the commit/rollback) it can then manage its
   * cache, notify BeanPersistListeners etc.
   * </p>
   */
  private static class JtaTxnListener implements Synchronization {

    private final TransactionManager transactionManager;

    private final SpiTransaction transaction;

    private final String serverName;

    private JtaTxnListener(TransactionManager transactionManager, SpiTransaction t) {
      this.transactionManager = transactionManager;
      this.transaction = t;
      this.serverName = transactionManager.getServerName();
    }

    @Override
    public void beforeCompletion() {
      // Future note: for JPA2 locking we will
      // have beforeCommit events to fire
    }

    @Override
    public void afterCompletion(int status) {

      switch (status) {
        case Status.STATUS_COMMITTED:
          if (logger.isDebugEnabled()) {
            logger.debug("Jta Txn [" + transaction.getId() + "] committed");
          }
          transactionManager.notifyOfCommit(transaction);
          // Remove this transaction object as it is completed
          DefaultTransactionThreadLocal.replace(serverName, null);
          break;

        case Status.STATUS_ROLLEDBACK:
          if (logger.isDebugEnabled()) {
            logger.debug("Jta Txn [" + transaction.getId() + "] rollback");
          }
          transactionManager.notifyOfRollback(transaction, null);
          // Remove this transaction object as it is completed
          DefaultTransactionThreadLocal.replace(serverName, null);
          break;

        default:
          if (logger.isDebugEnabled()) {
            logger.debug("Jta Txn [" + transaction.getId() + "] status:" + status);
          }
      }

      // No matter the completion status of the transaction, we release the connection we got from the pool.
      JdbcClose.close(transaction.getInternalConnection());
    }
  }

}
